agora inbox for [email protected]
help / color / mirror / Atom feedRe: Regression testing for psql
1095+ messages / 7 participants
[nested] [flat]
* Re: Regression testing for psql
@ 2010-05-25 02:51 Stephen Frost <[email protected]>
0 siblings, 1 reply; 1095+ messages in thread
From: Stephen Frost @ 2010-05-25 02:51 UTC (permalink / raw)
To: pgsql-hackers
* Stephen Frost ([email protected]) wrote:
> Add regression testing for psql backslash commands
>
> This patch adds rather extensive regression testing
> of the psql backslash commands. Hopefully this will
> minimize issues such as the one which cropped up
> recently with \h segfaulting. Note that we don't
> currently explicit check all the \h options and that
> pretty much any catalog changes will mean that this
> needs to also be updated. Still, it's a start, we can
> reduce the set of tests if that makes sense or they
> become a problem.
And.. it's way too big to send to the list. The patch is available
here:
http://snowman.net/~sfrost/psql-regress-help.patch
Of course, if people want to suggest tests that just shouldn't be
included, I can go through and strip things out.
Thanks,
Stephen
Attachments:
[application/pgp-signature] signature.asc (197B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-25 03:38 Robert Haas <[email protected]>
parent: Stephen Frost <[email protected]>
0 siblings, 2 replies; 1095+ messages in thread
From: Robert Haas @ 2010-05-25 03:38 UTC (permalink / raw)
To: Stephen Frost <[email protected]>; +Cc: pgsql-hackers
On Mon, May 24, 2010 at 10:51 PM, Stephen Frost <[email protected]> wrote:
> * Stephen Frost ([email protected]) wrote:
>> Add regression testing for psql backslash commands
>>
>> This patch adds rather extensive regression testing
>> of the psql backslash commands. Hopefully this will
>> minimize issues such as the one which cropped up
>> recently with \h segfaulting. Note that we don't
>> currently explicit check all the \h options and that
>> pretty much any catalog changes will mean that this
>> needs to also be updated. Still, it's a start, we can
>> reduce the set of tests if that makes sense or they
>> become a problem.
>
> And.. it's way too big to send to the list. The patch is available
> here:
>
> http://snowman.net/~sfrost/psql-regress-help.patch
>
> Of course, if people want to suggest tests that just shouldn't be
> included, I can go through and strip things out.
Well... I'm a little reluctant to believe that we should have 3.3M of
tests for the entire backend and 5M of tests just for psql. Then,
too, there's the fact that many of these tests fail on my machine
because my username is not sfrost, and/or because of row-ordering
differences on backslash commands without enough ORDER BY to fully
determine the output order.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise Postgres Company
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-25 10:23 Stephen Frost <[email protected]>
parent: Robert Haas <[email protected]>
1 sibling, 1 reply; 1095+ messages in thread
From: Stephen Frost @ 2010-05-25 10:23 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: pgsql-hackers
* Robert Haas ([email protected]) wrote:
> > Of course, if people want to suggest tests that just shouldn't be
> > included, I can go through and strip things out.
>
> Well... I'm a little reluctant to believe that we should have 3.3M of
> tests for the entire backend and 5M of tests just for psql. Then,
> too, there's the fact that many of these tests fail on my machine
> because my username is not sfrost, and/or because of row-ordering
> differences on backslash commands without enough ORDER BY to fully
> determine the output order.
Yeah, you know, I had fully intended to go grepping through the output
last night to check for things like that, but my wife decided I needed
sleep instead. :) Sorry about that. Still, it's more of a general
proposal than something I think should be committed as-is. Should we
try to deal with those kinds of differences, or just eliminate the tests
which are dependent on username, etc? It definitely strikes me that
there's a fair bit of code in psql we're not exercising in some fashion
in the regression suite... :/
Thanks,
Stephen
Attachments:
[application/pgp-signature] signature.asc (197B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-26 10:01 Peter Eisentraut <[email protected]>
parent: Stephen Frost <[email protected]>
0 siblings, 1 reply; 1095+ messages in thread
From: Peter Eisentraut @ 2010-05-26 10:01 UTC (permalink / raw)
To: Stephen Frost <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers
On tis, 2010-05-25 at 06:23 -0400, Stephen Frost wrote:
> * Robert Haas ([email protected]) wrote:
> > > Of course, if people want to suggest tests that just shouldn't be
> > > included, I can go through and strip things out.
> >
> > Well... I'm a little reluctant to believe that we should have 3.3M of
> > tests for the entire backend and 5M of tests just for psql. Then,
> > too, there's the fact that many of these tests fail on my machine
> > because my username is not sfrost, and/or because of row-ordering
> > differences on backslash commands without enough ORDER BY to fully
> > determine the output order.
>
> Yeah, you know, I had fully intended to go grepping through the output
> last night to check for things like that, but my wife decided I needed
> sleep instead. :) Sorry about that. Still, it's more of a general
> proposal than something I think should be committed as-is. Should we
> try to deal with those kinds of differences, or just eliminate the tests
> which are dependent on username, etc? It definitely strikes me that
> there's a fair bit of code in psql we're not exercising in some fashion
> in the regression suite... :/
Maybe pg_regress is not the right framework to test that sort of thing.
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-26 15:09 Stephen Frost <[email protected]>
parent: Peter Eisentraut <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Stephen Frost @ 2010-05-26 15:09 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers
* Peter Eisentraut ([email protected]) wrote:
> Maybe pg_regress is not the right framework to test that sort of thing.
Perhaps, but if not, then what? And how can we avoid writing a bunch of
new code that would then need to be checked itself..?
Thanks,
Stephen
Attachments:
[application/pgp-signature] signature.asc (197B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-26 19:19 Stephen Frost <[email protected]>
parent: Robert Haas <[email protected]>
1 sibling, 1 reply; 1095+ messages in thread
From: Stephen Frost @ 2010-05-26 19:19 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: pgsql-hackers
* Robert Haas ([email protected]) wrote:
> Then, too, there's the fact that many of these tests fail on my
> machine because my username is not sfrost,
I've updated the patch to address this, it's again at:
http://snowman.net/~sfrost/psql-regress-help.patch
If the size is still an issue, I could just go through and remove the
commands which return lots of records (that would also remove most of
the info about the catalog, minimizing the effect on this set of tests
when people change the catalog..). Downside there, of course, is that
we're not testing as many cases. Still, better something than nothing.
:)
> and/or because of row-ordering differences on backslash commands
> without enough ORDER BY to fully determine the output order.
I don't believe this was ever actually an issue. At least, I've run it
a number of times locally without issue. If anyone is still getting
differences when run against 9.0 HEAD, please let me know.
commit e301c873740816c5f3fd5437dcc559c880b8f404
Author: Stephen Frost <[email protected]>
Date: Wed May 26 15:02:28 2010 -0400
Add regression tests for psql backslash commands
This patch adds rather extensive regression testing
of the psql backslash commands. Hopefully this will
minimize issues such as the one which cropped up
recently with \h segfaulting. Note that we don't
currently explicit check all the \h options and that
many catalog changes will mean that this needs to also
be updated. Still, it's a start, we can reduce the
set of tests if that makes sense or they become a
problem. Also, any tests which would include the
owner of the database have been excluded.
Patch here:
http://snowman.net/~sfrost/psql-regress-help.patch
Thanks,
Stephen
Attachments:
[application/pgp-signature] signature.asc (197B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-26 19:52 alvherre <[email protected]>
parent: Stephen Frost <[email protected]>
0 siblings, 1 reply; 1095+ messages in thread
From: alvherre @ 2010-05-26 19:52 UTC (permalink / raw)
To: Stephen Frost <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers
Excerpts from Stephen Frost's message of mié may 26 15:19:59 -0400 2010:
> * Robert Haas ([email protected]) wrote:
> > Then, too, there's the fact that many of these tests fail on my
> > machine because my username is not sfrost,
>
> I've updated the patch to address this, it's again at:
> http://snowman.net/~sfrost/psql-regress-help.patch
Isn't this kind of test a pain to maintain? If somebody add a new SQL
command, it will affect the entire \h output and she'll have to either
apply the changes without checking them, or manually check the complete
list. I have only to add a new function to make the test fail ...
Also, having to exclude tests that mention the database owner means that
you're only testing a fraction of the commands, so any possible problem
has a large chance of going undetected. I mean, if we're going to test
this kind of thing, shouldn't we be using something that allows us to
ignore the db owner name? A simple wildcard in place of the owner name
would suffice ... or do we need a regex for some other reason?
The \h output normally depends on terminal width. Have you handled that
somehow?
(And if we want something like this, I think we should not have a single
huge file for the complete test, but a set of smaller files. I'd even
put the bunch in src/bin/psql/regress rather than the main regress dir.)
--
Álvaro Herrera <[email protected]>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-26 23:57 Stephen Frost <[email protected]>
parent: alvherre <[email protected]>
0 siblings, 1 reply; 1095+ messages in thread
From: Stephen Frost @ 2010-05-26 23:57 UTC (permalink / raw)
To: alvherre <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers
* alvherre ([email protected]) wrote:
> Excerpts from Stephen Frost's message of mié may 26 15:19:59 -0400 2010:
> > * Robert Haas ([email protected]) wrote:
> > > Then, too, there's the fact that many of these tests fail on my
> > > machine because my username is not sfrost,
> >
> > I've updated the patch to address this, it's again at:
> > http://snowman.net/~sfrost/psql-regress-help.patch
>
> Isn't this kind of test a pain to maintain? If somebody add a new SQL
> command, it will affect the entire \h output and she'll have to either
> apply the changes without checking them, or manually check the complete
> list. I have only to add a new function to make the test fail ...
Erm, last I checked, we already provide a diff output for the changes to
the regression output. That doesn't help when you add a new column to a
catalog, but that's a far cry from just adding some new SQL syntax or
adding a function.
> Also, having to exclude tests that mention the database owner means that
> you're only testing a fraction of the commands, so any possible problem
> has a large chance of going undetected. I mean, if we're going to test
> this kind of thing, shouldn't we be using something that allows us to
> ignore the db owner name? A simple wildcard in place of the owner name
> would suffice ... or do we need a regex for some other reason?
Yes, being able to use a regexp or something would be nice, but we don't
have those mechanics in the regression tests now (unless I missed
something).
> The \h output normally depends on terminal width. Have you handled that
> somehow?
I don't think that'll actually be an issue, but would love to hear from
people if it is.
> (And if we want something like this, I think we should not have a single
> huge file for the complete test, but a set of smaller files. I'd even
> put the bunch in src/bin/psql/regress rather than the main regress dir.)
The actual set of tests is rather small. The output is large, but
that's just because we have alot of things in the catalog.
Thanks,
Stephen
Attachments:
[application/pgp-signature] signature.asc (197B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-27 00:30 Tom Lane <[email protected]>
parent: Stephen Frost <[email protected]>
0 siblings, 2 replies; 1095+ messages in thread
From: Tom Lane @ 2010-05-27 00:30 UTC (permalink / raw)
To: Stephen Frost <[email protected]>; +Cc: alvherre <[email protected]>; Robert Haas <[email protected]>; pgsql-hackers
Stephen Frost <[email protected]> writes:
> * alvherre ([email protected]) wrote:
>> (And if we want something like this, I think we should not have a single
>> huge file for the complete test, but a set of smaller files. I'd even
>> put the bunch in src/bin/psql/regress rather than the main regress dir.)
> The actual set of tests is rather small. The output is large, but
> that's just because we have alot of things in the catalog.
It sounds to me like this is going to be like the rules regression test
writ large; specifically the part that dumps out view definitions for
all the built-in views. And that, quite frankly, has been a huge
maintenance burden and AFAIR has never once had any redeeming social
value in terms of catching a bug. If you're testing things that way,
don't. There might be some value in psql backslash command tests that
are designed to depend on just one or a few tables (or other appropriate
objects). Dumping large fractions of the catalogs will just be a net
loss.
regards, tom lane
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-27 00:36 Stephen Frost <[email protected]>
parent: Tom Lane <[email protected]>
1 sibling, 0 replies; 1095+ messages in thread
From: Stephen Frost @ 2010-05-27 00:36 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: alvherre <[email protected]>; Robert Haas <[email protected]>; pgsql-hackers
* Tom Lane ([email protected]) wrote:
> There might be some value in psql backslash command tests that
> are designed to depend on just one or a few tables (or other appropriate
> objects). Dumping large fractions of the catalogs will just be a net
> loss.
Fair enough, I can certainly do that pretty easily. Stripping out the
unqualified \d*S commands should result in a much, much, much smaller
output, with corresponding less changes due to catalog changes.
Thanks,
Stephen
Attachments:
[application/pgp-signature] signature.asc (197B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-27 01:25 Stephen Frost <[email protected]>
parent: Tom Lane <[email protected]>
1 sibling, 1 reply; 1095+ messages in thread
From: Stephen Frost @ 2010-05-27 01:25 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: alvherre <[email protected]>; Robert Haas <[email protected]>; pgsql-hackers
* Tom Lane ([email protected]) wrote:
> There might be some value in psql backslash command tests that
> are designed to depend on just one or a few tables (or other appropriate
> objects).
Updated, much much smaller, patch attached. Also available, again, at
http://snowman.net/~sfrost/psql-regress-help.patch
Basically, I removed anything that would produce data directly from
the catalogs by trying to find a 'none' object which matched. This
still goes through alot of the same setup and query, it's just that
there aren't any results.
Thanks,
Stephen
Attachments:
[application/octet-stream] psql-regress-help.patch.gz (6.7K, ../../[email protected]/2-psql-regress-help.patch.gz)
download
[application/pgp-signature] signature.asc (197B, ../../[email protected]/3-signature.asc)
download
^ permalink raw reply [nested|flat] 1095+ messages in thread
* Re: Regression testing for psql
@ 2010-05-30 00:37 Selena Deckelmann <[email protected]>
parent: Stephen Frost <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Selena Deckelmann @ 2010-05-30 00:37 UTC (permalink / raw)
To: Stephen Frost <[email protected]>; +Cc: Tom Lane <[email protected]>; alvherre <[email protected]>; Robert Haas <[email protected]>; pgsql-hackers
On Wed, May 26, 2010 at 6:25 PM, Stephen Frost <[email protected]> wrote:
> * Tom Lane ([email protected]) wrote:
>> There might be some value in psql backslash command tests that
>> are designed to depend on just one or a few tables (or other appropriate
>> objects).
>
> Updated, much much smaller, patch attached. Also available, again, at
> http://snowman.net/~sfrost/psql-regress-help.patch
>
> Basically, I removed anything that would produce data directly from
> the catalogs by trying to find a 'none' object which matched. This
> still goes through alot of the same setup and query, it's just that
> there aren't any results.
Is this something to be added to 2010-07 commitfest?
-selena
--
http://chesnok.com/daily - me
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size.
In psql, include a new "Free" column in \db+ output.
---
doc/src/sgml/func.sgml | 21 ++++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++--
src/include/catalog/pg_proc.dat | 8 +++
5 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
--
2.47.2
--vqKhBUYnSUFh3RIa--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--mxigUiQ2jY+Id+Rf--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func.sgml | 21 ++++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++
src/bin/psql/describe.c | 11 ++-
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 ++++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 163 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.47.2
--65NGEM4fhwMw73YU--
^ permalink raw reply [nested|flat] 1095+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29 Christoph Berg <[email protected]>
0 siblings, 0 replies; 1095+ messages in thread
From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
doc/src/sgml/func/func-admin.sgml | 21 +++++
doc/src/sgml/ref/psql-ref.sgml | 2 +-
src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++
src/bin/psql/describe.c | 32 +++++--
src/include/catalog/pg_proc.dat | 8 ++
src/test/regress/expected/tablespace.out | 21 +++++
src/test/regress/sql/tablespace.sql | 10 +++
7 files changed, 189 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ {
+ if (errno == ENOENT)
+ return -1;
+ else
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+ }
+
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
appendPQExpBuffer(&buf,
"SELECT spcname AS \"%s\",\n"
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
- " pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+ " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
gettext_noop("Name"),
gettext_noop("Owner"),
gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+ " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+ " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+ " ELSE 'No Access'"
+ " END as \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
appendPQExpBufferStr(&buf,
- "\nFROM pg_catalog.pg_tablespace\n");
+ "\nFROM pg_catalog.pg_tablespace tblspc\n");
+ if (verbose)
+ appendPQExpBufferStr(&buf,
+ "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+ " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
if (!validateSQLNamePattern(&buf, pattern, false, false,
NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
printACLColumn(&buf, "d.datacl");
if (verbose)
appendPQExpBuffer(&buf,
- ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+ ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+ " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
" ELSE 'No Access'\n"
" END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
{random_page_cost=3.0}
(1 row)
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size
+----------+----------+--------------------
+ t | t | 0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR: tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column?
+----------+----------+----------
+ t | t | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR: tablespace "missing" does not exist
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
-- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
-- check to see the parameter was used
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+ pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+ pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+ pg_tablespace_avail('pg_global') > 1_000_000,
+ pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
-- drop the tablespace so we can re-use the location
DROP TABLESPACE regress_tblspacewith;
--
2.53.0
--sW4eVZZF7egqrnKF--
^ permalink raw reply [nested|flat] 1095+ messages in thread
end of thread, other threads:[~2025-03-14 15:29 UTC | newest]
Thread overview: 1095+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2010-05-25 02:51 Re: Regression testing for psql Stephen Frost <[email protected]>
2010-05-25 03:38 ` Robert Haas <[email protected]>
2010-05-25 10:23 ` Stephen Frost <[email protected]>
2010-05-26 10:01 ` Peter Eisentraut <[email protected]>
2010-05-26 15:09 ` Stephen Frost <[email protected]>
2010-05-26 19:19 ` Stephen Frost <[email protected]>
2010-05-26 19:52 ` alvherre <[email protected]>
2010-05-26 23:57 ` Stephen Frost <[email protected]>
2010-05-27 00:30 ` Tom Lane <[email protected]>
2010-05-27 00:36 ` Stephen Frost <[email protected]>
2010-05-27 01:25 ` Stephen Frost <[email protected]>
2010-05-30 00:37 ` Selena Deckelmann <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox